/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.resources.internal.services;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.security.Permission;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.elwiki.IWikiConstants;
import org.elwiki.permissions.PagePermission;
import org.elwiki.permissions.PermissionFactory;
import org.elwiki_data.AttachmentContent;
import org.elwiki_data.WikiPage;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;

import erwiki.api.attachment.AttachmentManager;
import erwiki.api.auth.AuthorizationManager;
import erwiki.api.auth.ISessionMonitor;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiContext;
import erwiki.api.exceptions.ProviderException;
import erwiki.api.pages.PageManager;
import erwiki.api.rwc.RWC;
import erwiki.api.rwc.engune.RwcServlet;
import erwiki.api.ui.PageCommand;
import erwiki.api.ui.progress.ProgressManager;
import erwiki.configuration.IWikiConfiguration;
import erwiki.util.HttpUtil;
import erwiki.util.TextUtil;

/**
 * This is the chief JSPWiki attachment management servlet. It is used for both uploading new
 * content and downloading old content. It can handle most common cases, e.g. check for
 * modifications and return 304's as necessary.
 * <p>
 * Authentication is done using JSPWiki's normal AAA framework.
 * <p>
 * This servlet is also capable of managing dynamically created attachments.
 *
 *
 * @since 1.9.45.
 */
//@formatter:off
@Component(
  service=Servlet.class,
  property= {
  	HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN + "=/attach/*",
  	HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=("
  	+ HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + ContextHelperPart.CONTEXT_NAME +")"},
  scope=ServiceScope.PROTOTYPE,
  name="web.AttachmentServlet")
//@formatter:on
public class AttachmentServlet extends RwcServlet {

	private static final Logger log = Logger.getLogger(AttachmentServlet.class);

	private static final int BUFFER_SIZE = 8192;

	private static final long serialVersionUID = 3257282552187531320L;

	private static final String HDR_VERSION = "version";
	// private static final String HDR_NAME = "page";

	/** Default expiry period is 1 day */
	protected static final long DEFAULT_EXPIRY = 1 * 24 * 60 * 60 * 1000;

	/** Stores configuration. */
	@Reference
	private IWikiConfiguration wikiConfiguration;

	@Reference
	private Engine m_engine;

	@Reference
	private ISessionMonitor sessionMonitor;

	@Reference
	private AuthorizationManager authorizationManager;

	@Reference
	private AttachmentManager attachmentManager;

	@Reference
	private ProgressManager progressManager;

	@Reference
	private PageManager pageManager;

	@Deprecated
	/** The maximum size that an attachment can be. */
	private int m_maxSize = Integer.MAX_VALUE;

	/** List of attachment types which are allowed */
	private String[] m_allowedPatterns;

	private String[] m_forbiddenPatterns;

	//
	// Not static as DateFormat objects are not thread safe.
	// Used to handle the RFC date format = Sat, 13 Apr 2002 13:23:01 GMT
	//
	// private final DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");

	@Activate
	protected void startup() {
		log.debug("«web» start " + AttachmentServlet.class.getSimpleName());
	}

	@Override
	protected Engine getEngine() {
		return m_engine;
	}

	@Override
	protected ISessionMonitor getSessionMonitor() {
		return sessionMonitor;
	}

	/**
	 * Initializes the servlet from Engine properties.
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		String allowed = "";// wikiConfiguration.getStringProperty(AttachmentManager.PROP_ALLOWEDEXTENSIONS, null);
		m_maxSize = 100000; // :FVK: wikiConfiguration.getIntegerProperty(AttachmentManager.PROP_MAXSIZE,
							// Integer.MAX_VALUE);

		if (allowed != null && allowed.length() > 0) {
			m_allowedPatterns = allowed.toLowerCase().split("\\s");
		} else {
			m_allowedPatterns = new String[0];
		}

		String forbidden = ""; // wikiConfiguration.getStringProperty(AttachmentManager.PROP_FORBIDDENEXTENSIONS, null);
		if (forbidden != null && forbidden.length() > 0) {
			m_forbiddenPatterns = forbidden.toLowerCase().split("\\s");
		} else {
			m_forbiddenPatterns = new String[0];
		}
	}

	private boolean isTypeAllowed(String name) {
		if (name == null || name.length() == 0) {
			return false;
		}

		name = name.toLowerCase();

		for (int i = 0; i < m_forbiddenPatterns.length; i++) {
			if (name.endsWith(m_forbiddenPatterns[i]) && m_forbiddenPatterns[i].length() > 0) {
				return false;
			}
		}

		for (int i = 0; i < m_allowedPatterns.length; i++) {
			if (name.endsWith(m_allowedPatterns[i]) && m_allowedPatterns[i].length() > 0) {
				return true;
			}
		}

		return m_allowedPatterns.length == 0;
	}

	/**
	 * Serves a GET with two parameters: 'wikiname' specifying the wikiname of the attachment, 'version'
	 * specifying the version indicator.
	 *
	 */
	// FIXME: Messages would need to be localized somehow.
	@Override
	// @LoggedMethod
	protected void serviceDoGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
		RWC.INSTANCE.setCommand(PageCommand.ATTACH);

		String version = req.getParameter(HDR_VERSION);
		String nextPage = req.getParameter("nextpage");
		String attachmentId = req.getParameter("atchid");// TODO: вывести в глобальную константу.
		int ver = IWikiConstants.LATEST_VERSION;

		if (StringUtils.isBlank(attachmentId)) {
			log.info("Invalid attachment ID.");
			res.sendError(HttpServletResponse.SC_BAD_REQUEST);
			return;
		}

		try (OutputStream out = res.getOutputStream()) {
			log.debug("Attempting to download attachment by ID " + attachmentId + ", version " + version);
			if (version != null) {
				ver = Short.parseShort(version);
			}

			AttachmentContent attContent = attachmentManager.getAttachmentContentById(attachmentId, ver);
			if (attContent != null) {
				/* Check if the user has permissions for this attachment.
				 */
				WikiPage wikiPage = attContent.getPageAttachment().getWikipage();
				Permission permission = PermissionFactory.getPagePermission(wikiPage, PagePermission.VIEW_ACTION);
				if (!this.authorizationManager.checkPermission(RWC.INSTANCE.getWikiSession(), permission)) {
					log.debug("User does not have permission for view attachment (PAGE VIEW action).");
					res.sendError(HttpServletResponse.SC_FORBIDDEN);
					return;
				}

				/* Check if the client already has a version of this attachment.
				 */
				/*:FVK: TODO:
				if( HttpUtil.checkFor304( req, attachmentName, attContent.getCreationDate() ) ) {
				    log.debug( "Client has latest version already, sending 304..." );
				    res.sendError( HttpServletResponse.SC_NOT_MODIFIED );
				    return;
				}*/

				String fileName = attContent.getPageAttachment().getName();
				String mimetype = getMimeType(fileName);
				res.setContentType(mimetype);

				/* We use 'inline' instead of 'attachment' so that user agents
				 * can try to automatically open the file.
				 */
				res.addHeader("Content-Disposition", "inline; filename=\"" + fileName + "\";");
				res.addDateHeader("Last-Modified", attContent.getCreationDate().getTime());

				// If a size is provided by the provider, report it.
				if (attContent.getSize() >= 0) {
					res.setContentLength((int) attContent.getSize());
				}

				try (InputStream in = this.attachmentManager.getAttachmentStream(attContent)) {
					int read;
					byte[] buffer = new byte[BUFFER_SIZE];

					while ((read = in.read(buffer)) > -1) {
						out.write(buffer, 0, read);
					}
				}

				log.debug("Attachment " + fileName + " sent to " + req.getRemoteUser() + " on "
						+ HttpUtil.getRemoteAddress(req));

				if (nextPage != null) {
					res.sendRedirect(validateNextPage(TextUtil.urlEncodeUTF8(nextPage),
							m_engine.getURL(WikiContext.WIKI_ERROR, "", null)));
				}

			} else {
				String msg = "Attachment ID '" + attachmentId + "', version " + ver + " does not exist.";
				log.info(msg);
				res.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
			}
		} catch (ProviderException pe) {
			log.debug("Provider failed while reading", pe);
			//
			// This might fail, if the response is already committed. So in that
			// case we just log it.
			//
			sendError(res, "Provider error: " + pe.getMessage());
		} catch (NumberFormatException nfe) {
			log.warn("Invalid version number: " + version);
			res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid version number");
		} catch (SocketException se) {
			//
			// These are very common in download situations due to aggressive
			// clients. No need to try and send an error.
			//
			log.debug("I/O exception during download", se);
		} catch (IOException ioe) {
			//
			// Client dropped the connection or something else happened.
			// We don't know where the error came from, so we'll at least
			// try to send an error and catch it quietly if it doesn't quite work.
			//
			log.debug("I/O exception during download", ioe);
			sendError(res, "Error: " + ioe.getMessage());
		}
	}

	void sendError(HttpServletResponse res, String message) throws IOException {
		try {
			res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
		} catch (IllegalStateException e) {
			// ignore
		}
	}

	/**
	 * Возвращает MIME тип для заданного файла.
	 *
	 * @param fileName Имя объективного файла.
	 * @return Допустимый mime-тип или "application/binary", если файл не распознан.
	 */
	private static String getMimeType(String fileName) {
		String mimetype = null;

		HttpServletRequest req = RWC.INSTANCE.getHttpRequest();
		if (req != null) {
			ServletContext s = req.getSession().getServletContext();
			if (s != null) {
				mimetype = s.getMimeType(fileName.toLowerCase());
			}
		}

		if (mimetype == null) {
			mimetype = "application/binary";
		}

		return mimetype;
	}

	/**
	 * Validates the next page to be on the same server as this webapp. Fixes [JSPWIKI-46].
	 */
	private String validateNextPage(String nextPage, String errorPage) {
		if (nextPage.contains("://")) {
			// It's an absolute link, so unless it starts with our address, we'll log an error.
			if (!nextPage.startsWith(m_engine.getWikiConfiguration().getBaseURL())) {
				log.warn("Detected phishing attempt by redirecting to an unsecure location: " + nextPage);
				nextPage = errorPage;
			}
		}

		return nextPage;
	}

}
